[BE201] 後端中階:middleware (上)


Posted by s103071049 on 2021-08-06

什麼是 middleware(中間件)?

Express 是一個本身功能極簡的路由與中介軟體 Web 架構:本質上,Express 應用程式是一系列的中介軟體函數呼叫。摘自官網

上述的中介軟體就是 middleware。

白話版:express 這套框架處理從收到 request 一直到發出 response 中間就是經過一系列 middleware 處理然後產生 response。

舉例來說,之前學習 express 的時候,做的重構版 index.js 裡面 app.get('/todos', todoController.getAll),在 /todos 後就交給 todoController.getAll,我們可以將 todoController.getAll 看成就是一個 middleware。

在這個 middleware 裡面永遠都可以接收到 req, res 兩個參數,然後去輸出我想要的東西。

const todoController = {
  getAll: (req, res) => {
     // 將資料從 model 拿出來
    todoModel.getAll((err, result) => {
      if (err) return console.log(err)
      res.render('todos', {
        todos: result
      })
    })
  },
  get: (req, res) => {
    const id = req.params.id
    // 回傳的資料會是 array
    const todo = todoModel.get(id, (err, result) => {
      if (err) return console.log(err)
      res.render('todo', {
        todo: result[0]
      })
    })
  }
}

甚麼是一系列呢 ?

app.use( ) 表示整個應用程式都可以用這個 middleware。app.use((req, res, next) => {}),next 表示將控制權發到下一個 middleware 去。

index.js 加入這一行

app.use((req, res) => {
  console.log('Time : ', new Date())
  res.end()
})

重新整理 http://localhost:5001/todos,網頁一片白,但 console 出現 Time : 2021-08-06T08:55:17.380Z,每重新整理 console 就會再 log 一次。

因為沒有 call next,所以本來的 response 就沒有了。call next 表示將控制權交給下一個 middleware。

app.use((req, res, next) => {
  console.log('Time : ', new Date())
  next()
})

這次重新整理網頁就有東西,console 一樣會印出現在時間

middleware 在 express 的功用

express 內建是沒有去解析 delete、post 的內容,預設是沒有解析 request body 的內容,這就要靠 middleware 去完成。

express 內建也沒有 session 管理機制,所以也要靠 middleware。

express 內建有 req.query 可以抓網址列的 query string。但如果是 post 就必須依靠 middleware。

  • url:http://localhost:5001/test?a=1&b=3,console 呈現 { a: '1', b: '3' }
app.get('/test', (req, res) => {
  console.log(req.query)
})

middleware 是有順序性的,

假設我們要做一個簡單的權限管理機制,只有在網址列上看到 admin,才會看到 todo 的東西。

最直覺寫法:兩個 controller 裡面都加入同一個程式碼

controllers 裡面的 todo.js

function checkPermission(req) {
  if (req.query.admin === '1') {
    return true
  } return false
}
===
function checkPermission(req) {
  return req.query.admin === '1'
}

在 method 裡面加入權限檢查 if(!checkPermission(req)) return res.end()

const todoModel = require('../models/todo')
function checkPermission(req) {
  return req.query.admin === '1'
}
const todoController = {
  getAll: (req, res) => {
    if(!checkPermission(req)) return res.end()
    todoModel.getAll((err, result) => {
      if (err) return console.log(err)
      res.render('todos', {
        todos: result
      })
    })
  },
  get: (req, res) => {
    if (!checkPermission(req)) return res.end()
    const id = req.params.id
    const todo = todoModel.get(id, (err, result) => {
      if (err) return console.log(err)
      res.render('todo', {
        todo: result[0]
      })
    })
  }
}

現在,http://localhost:5001/todos 網頁一片白,傳入 ?admin=1 => http://localhost:5001/todos?admin=1,就看的到東西了。

但 function 一多,會變得很煩,所以不是個好做法

以 middleware 寫

.use((req, res, next) => {})

瀏覽器跑 http://localhost:5001/todos?admin=1 會顯示畫面、但 admin 改成 2 就會顯示 Error

app.use 是整個 app 都會被影響到

// index.js 加入這段
app.use((req, res, next) => {
  if (req.query.admin === '1') {
    next()
  } else {
    res.end('Error')
  }
})

可以對個別的路由進行添加與處理

function checkPermission(req, res, next) {
  if (req.query.admin === '1') {
    next()
  } else {
    res.end('Error')
  }
}
app.get('/todos', checkPermission, todoController.getAll) // 只有這個路由會被影響

app.get('/todos/:id', todoController.get)

瀏覽器 http://localhost:5001/todos?admin=1 會顯示東西、http://localhost:5001/todos 出現 Error、但只有單一 todo 沒有問題 ex:http://localhost:5001/todos/2,因為單一 todo 沒有加上 checkPermission 的限制。

小結

  1. middleware 的使用:接收 request、response,next 是表示是否把控制權交到下一個 middleware 去

解析 Request 必備:body-parser

非常重要的 middleware:body-parser

因為 express 的內建只能拿到 url 的 query string,所以如果 post 拿不到東西,但藉由 body-parser 這個 middleware 我可以拿到 request body 裡面的資料,換句話說 post 過來的東西我拿的到了。

  1. 安裝指令:npm install body-parser
  2. 使用方法:參考文件與下方範例
  • 引入進來 const bodyParser = require('body-parser')
  • 決定要 parse 哪一種 content-type,一般來說下面兩個都會用到所以一起 parse 進來。
  • 加完 content-type 後可以用 req.body 拿資料
// parse application/x-www-form-urlencoded 
// 一般在瀏覽器上 post 的資料會是這個 content-type
app.use(bodyParser.urlencoded({ extended: false }))

// parse application/json
// ajax 會是這個 content-type
app.use(bodyParser.json())

實作一個新增 todo 的功能

  1. index.js 新增一個 app.get('/', todoController.addTodo)
  2. controller 新增 addTodo。這裡只是 render 頁面,並不是真的處理 addTodo 動作 addTodo: (req, res) => { res.render('addTodo')}
  3. views 新增 addTodo.ejs
  4. 現在 git bash 執行 node index.js => 瀏覽器去到根目錄網頁會 render addTodo.ejs => 輸入東西提交後會顯示 Cannot POST /todos,因為我們還沒處理 route 所以 express 也不知道該怎麼辦
  5. 在 index.js 新增一個路由 => app.post('/todos', todoController.newTodo) => controllers 裡面建立一個 newTodo,用 req.body.input的 name 將東西拿出來,拿完後先進行輸出看結果是否正確 => 正確的印出我輸入的東西
// addTodo.ejs
<h1>Add Todo</h1>
<form method = "POST" action="/todos">
  Content: <input type="text" name="content"/>
  <input type="submit"/>
</form>
// controller 裡面的 todo.js
  newTodo: (req, res) => {
    const content = req.body.content
    res.end(content) 
  }
  1. 若不加 content-type => TypeError: Cannot read property 'content' of undefined,也就是 request.body 是 undefined => 因為沒有用這個 middleware,所以沒有去做 request.body 的解析,因此就沒有東西出現 undefined
  2. controller 裡的 newTodo 拿到東西後去 call todoModel.add( )
  3. 處理 todoModel.add( ) => 到 models 裡進行 todo.js 的處理
  newTodo: (req, res) => {
    const content = req.body.content
    todoModel.add(content, (err) => {
      if (err) return console.log(err)
      res.redirect('/todos')
    })
  }

models 裡的 todo.js

  add: (content, cb) => {
    db.query(
      'insert into todos(content) values(?)', [content],
      (err, results) => {
        if (err) return cb(err)
        cb(null)
      }
    )
  }

view 裡面 todos.ejs 加上 <a href="/">add todo</a>

小結

  1. MVC 後端小專案、路由明確
  2. 一定要有 body-parser 才能處理 post 過來的資料。

負責管理 Session 的 Session middleware

  1. npm install express-session
  2. 一樣在 index.js 上 require 進來 const session = require('express-session')
  3. 建立 session 的 middleware
// 參數待查
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}))

用法很像 php,背後機制可以看 source code

$_SESSION['abc'] // php 
req.session.abc // 這邊
  1. 製作 login,一律是 req.session
  2. addTodo 加上參數 isLogin
app.get('/login', (req, res) => {
  res.render('login')
})
app.post('/login', (req, res) =>{
  if (req.body.password === 'abc') {
    req.session.isLogin = true
    res.redirect('/')
  } else {
    res.redirect('/login')
  }
})
  addTodo: (req, res) => {
    res.render('addTodo', {
      isLogin: req.session.isLogin
    })
  }
  1. 調整 view 裡面的 addTodo.ejs
  2. 製作 loging.ejs
// addTodo.ejs 加上下列 code
<% if (isLogin) {%>
  你已經登入
<% } else { %>
  尚未登入成功
<% } %>
// login.ejs
<h1>Login</h1>
<form method="POST" action="/login">
  password: <input type="password" name="password"/>
  <input type="submit">
</form>
  1. 製作 logout 功能
  2. view 加上 logout 按鈕
// index.js
app.get('/logout', (req, res) => {
  req.session.isLogin = false
  res.redirect('/')
})
// addTodo.ejs
<h1>Add Todo</h1>
<% if (isLogin) {%>
  你已經登入 <a href="/logout">logout</a>
<% } else { %>
  尚未登入成功
<% } %>

小結

  1. 熟悉 session 用法
  2. 思考問題:如果我需要在每個頁面都顯示是否登入,每個 render 的地方都要加 isLogin: req.session.isLogin,所以我可以怎麼辦呢?

顯示錯誤訊息神器:connect-flash

flash messenger,Messages are written to the flash and cleared after being displayed to the user,背後用了 session 機制。

實作過程可參考 source code

  1. npm install connect-flash
  2. First, setup sessions
  3. 引入 const flash = require('connect-flash')
  4. 加上 app.use(flash())
  5. 實際使用給予 key、value 就可以設置訊息,就可以跨頁面讀訊息 req.flash('info', 'Flash is back!')
  6. login.ejs 補上 <h2><%= errorMessage %></h2>

因為會導回根目錄頁面,所以在這邊這麼做。使用方式很像 session

// index.js
app.get('/login', (req, res) => {
  res.render('login', {
    errorMessage: req.flash('errorMessage') // 拿出資料
  })
})

app.post('/login', (req, res) =>{
  if (req.body.password === 'abc') {
    req.session.isLogin = true
    res.redirect('/')
  } else {
    req.flash('errorMessage', 'Password Incorrect') // 寫入資料
    res.redirect('/login')
  }
})

如果這個錯誤訊息在很多地方都用的到,express 提供了一個捷徑,讓我們自定義自己的 middleware

給 view 用的 global 變數:res.locals

res.locals 裡面的東西可以在 view 裡面拿到。views 可以用任何來自 request.locals 的東西,就像全域變數

拿 session、flash 都是用 req

app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}))
// 將它放在 app.use(session 後面
app.use((req, res, next) => {
  res.locals.isLogin = req.session.isLogin || false
  res.locals.errorMessage = req.flash('errorMessage')
  next() // 少了這個,request 會卡在這邊
})
// index.js
app.get('/login', (req, res) => {
  res.render('login')
})
// controllers- todo.js
addTodo: (req, res) => {
  res.render('addTodo')
}
//

小結

  1. 運用 req.flash 實作 flash messenger
  2. 放在 res.locals 的東西,就可以在 view 裡面直接被存取到,就不用每個 render 的地方都把東西放進去。是否登入、錯誤訊息都很適合放在這邊。

目標

// 使用 express 框架,server 跑完要連到 db
// 後端資料庫的資料、使用 view 框架、使用 controllers
// get 路由 1. todos 所有資料 2. todos/id 拿到個別資料 3. bye 不用 mvc 架構呈現 bye bye
// 使用 middle 1. 權限管理 2. 新增 todo (用 body parser 處理 post 過來的資料) 3. login 機制 4. flash messenger

#Middleware







Related Posts

2019 Web Backend 面試總結

2019 Web Backend 面試總結

Elevate Your Dermatology Practice with the Electric Dermatology Chair

Elevate Your Dermatology Practice with the Electric Dermatology Chair

Get讀取與Post傳送比較與傳送原理

Get讀取與Post傳送比較與傳送原理


Comments